Hooks 与 TypeScript

概述

在本教程中,我们将深入探讨如何在 React 项目中结合使用 Hooks 和 TypeScript。Hooks 是 React 16.8 引入的新特性,允许你在不编写类的情况下使用状态和其他 React 特性。TypeScript 则是一种静态类型检查器,可以为 JavaScript 添加类型注解,从而提高代码的可维护性和可读性。

开发环境搭建

在开始之前,确保你已经安装了 Node.js 和 TypeScript 编译器。你可以通过以下命令来安装 TypeScript:

npm install -g typescript

接下来,使用 Create React App 创建一个支持 TypeScript 的项目:

npx create-react-app my-app --template typescript
cd my-app

第一个 TypeScript 程序

在 src 目录下创建一个新的文件 App.tsx,并编写以下代码:

import React from 'react';

const App: React.FC = () => {
  return (
    <div>
      <h1>Hello, TypeScript!</h1>
    </div>
  );
};

export default App;

这个简单的组件展示了如何在 TypeScript 中定义一个 React 函数组件。React.FC 是 React 函数组件的类型注解。

基本类型和变量声明

TypeScript 允许你为变量、函数参数和返回值添加类型注解。以下是一些基本类型的示例:

let count: number = 0;
let message: string = "Hello, TypeScript!";
let isActive: boolean = true;
let numbers: number[] = [1, 2, 3];
let user: { name: string, age: number } = { name: "Alice", age: 30 };

TypeScript 配置文件 (tsconfig.json)

tsconfig.json 是 TypeScript 项目的配置文件。你可以在项目根目录下创建一个 tsconfig.json 文件,并添加以下配置:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "react"
  },
  "include": ["src"]
}

基本类型详解

数字类型

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

字符串类型

let color: string = "blue";
color = 'red';

布尔类型

let isDone: boolean = false;

数组类型

let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

元组类型

let x: [string, number];
x = ["hello", 10];

枚举类型

枚举类型允许你定义一组命名的常量。

enum Color {
  Red,
  Green,
  Blue
}
let c: Color = Color.Green;

Any 和 Unknown 类型

any 类型允许你绕过类型检查,而 unknown 类型则要求你在使用之前进行类型检查。

let notSure: any = 4;
notSure = "maybe a string instead";

let uncertain: unknown = 4;
if (typeof uncertain === "string") {
  console.log(uncertain.length);
}

Void, Null 和 Undefined

void 表示没有返回值的函数,null 和 undefined 是所有类型的子类型。

function warnUser(): void {
  console.log("This is my warning message");
}

let u: undefined = undefined;
let n: null = null;

Never 类型

never 类型表示永远不会发生的值。

function error(message: string): never {
  throw new Error(message);
}

类型断言

类型断言允许你告诉编译器某个值的具体类型。

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

函数声明和表达式

你可以使用函数声明或函数表达式来定义函数。

function add(x: number, y: number): number {
  return x + y;
}

let myAdd = function(x: number, y: number): number {
  return x + y;
};

可选参数和默认参数

TypeScript 支持可选参数和默认参数。

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}

function buildName2(firstName: string, lastName = "Smith"): string {
  return firstName + " " + lastName;
}

剩余参数

剩余参数允许你将多个参数收集到一个数组中。

function buildName3(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

函数重载

函数重载允许你定义多个函数签名。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  return a + b;
}

This 和箭头函数

箭头函数不会改变 this 的指向。

let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function() {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);
      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  }
};

接口定义和使用

接口用于定义对象的形状。

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

可选属性和只读属性

接口中的属性可以是可选的或只读的。

interface SquareConfig {
  color?: string;
  width?: number;
}

interface Point {
  readonly x: number;
  readonly y: number;
}

函数类型接口

接口可以描述函数类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};

类类型接口

接口可以描述类的结构。

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

继承接口

接口可以继承其他接口。

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

类型别名 vs 接口

类型别名和接口都可以用来定义类型,但它们有一些区别。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

interface Person {
  name: string;
  age: number;
}

类的定义和实例化

类是面向对象编程的基础。

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

继承和多态

类可以继承其他类,并实现多态。

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

访问修饰符

TypeScript 支持 public、private 和 protected 访问修饰符。

class Animal {
  public name: string;
  public constructor(theName: string) {
    this.name = theName;
  }
  public move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

静态属性和方法

静态属性和方法属于类本身,而不是类的实例。

class Grid {
  static origin = { x: 0, y: 0 };
  calculateDistanceFromOrigin(point: { x: number; y: number }) {
    let xDist = point.x - Grid.origin.x;
    let yDist = point.y - Grid.origin.y;
    return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
  }
  constructor(public scale: number) {}
}

抽象类

抽象类不能被实例化,只能被继承。

abstract class Department {
  constructor(public name: string) {}

  printName(): void {
    console.log("Department name: " + this.name);
  }

  abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {
  constructor() {
    super("Accounting and Auditing"); // 在派生类的构造函数中必须调用 super()
  }

  printMeeting(): void {
    console.log("The Accounting Department meets each Monday at 10am.");
  }

  generateReports(): void {
    console.log("Generating accounting reports...");
  }
}

交叉类型

交叉类型可以将多个类型合并为一个类型。

function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for (let id in first) {
    (<any>result)[id] = (<any>first)[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<any>result)[id] = (<any>second)[id];
    }
  }
  return result;
}

联合类型

联合类型表示一个值可以是多种类型之一。

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

类型保护和类型区分

类型保护允许你在运行时检查变量的类型。

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}

可为 null 的类型

TypeScript 支持可为 null 的类型。

let s = "foo";
s = null; // 错误, 'null' 不能赋值给 'string'
let sn: string | null = "bar";
sn = null; // 可以

字面量类型

字面量类型允许你定义具体的值作为类型。

let x: "hello" = "hello";
x = "hello"; // 可以
x = "howdy"; // 错误

索引类型和映射类型

索引类型和映射类型允许你操作对象的键和值。

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
  name: string;
  age: number;
}

let person: Person = {
  name: "Jarid",
  age: 35
};

let strings: string[] = pluck(person, ["name"]); // 可以

泛型函数

泛型函数允许你编写可以处理多种类型的函数。

function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("myString");

泛型接口

泛型接口允许你定义可以处理多种类型的接口。

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

泛型类

泛型类允许你定义可以处理多种类型的类。

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
  return x + y;
};

泛型约束

泛型约束允许你限制泛型参数的类型。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 现在我们知道它有 .length 属性,所以不再报错
  return arg;
}

ES6 模块系统

ES6 模块系统允许你将代码组织成模块。

// math.ts
export function add(x: number, y: number): number {
  return x + y;
}

// app.ts
import { add } from "./math";

console.log(add(1, 2)); // 3

命名空间

命名空间允许你将代码组织成逻辑分组。

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;
  const numberRegexp = /^[0-9]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}

模块解析策略

TypeScript 支持不同的模块解析策略。

{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

类装饰器

类装饰器允许你在类定义时添加元数据。

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting